local bininsert=require'zdc.tblutil'.bininsert
local binsearch=require'zdc.tblutil'.binsearch
local bintailset=require'zdc.tblutil'.bintailset
module(..., package.seeall)

CornExp = {}
CornExp_mt ={__metatable = {}, __index = CornExp}

local SECOND = 0;
local MINUTE = 1;
local HOUR = 2;
local DAY_OF_MONTH = 3;
local MONTH = 4;
local DAY_OF_WEEK = 5;
local YEAR = 6;
local ALL_SPEC_INT = 99; -- '*'
local NO_SPEC_INT = 98; -- '?'
local ALL_SPEC = ALL_SPEC_INT
local NO_SPEC = NO_SPEC_INT
local MAX_YEAR=2050

local monthMap={JAN=1;FEB=2;MAR=3;APR=4;MAY=5;JUN=6;JUL=7;AUG=8;SEP=9;OCT=10;NOV=11;DEC=12}
local dayMap={SUN=1;MON=2;TUE=3;WED=4;THU=5;FRI=6;SAT=7}

function new(cornstr)
	local obj = setmetatable({}, CornExp_mt)
	local _set={
		[SECOND]={};
		[MINUTE]={};
		[HOUR]={};
		[DAY_OF_MONTH]={};
		[MONTH]={};
		[DAY_OF_WEEK]={};
		[YEAR]={};
	}
	obj.cronExpression=cornstr:upper()
	obj._set=_set
	obj.lastdayOfWeek = false;
	obj.nthdayOfWeek = 0;
	obj.lastdayOfMonth = false;
	obj.nearestWeekday = false;
	obj.lastdayOffset = 0;
	obj.expressionParsed = false;
	return obj
end

local function getValue(v,s,i)
	local c=s:sub(i,i)
	local s1=''..v
	while c>='0' and c<='9' do
		s1=s1..c
		i=i+1
		if i>#s then break end
		c=s:sub(i,i)
	end
	return tonumber(s1),(i<=#s) and i or i+1
end

local function getSet(self,type)
	return self._set[type]
end

local function addToSet(self,val,vend,incr,type)
	val=tonumber(val)
	local set = getSet(self,type);

	if (type == SECOND or type == MINUTE) then
		if ((val < 0 or val > 59 or vend > 59) and (val ~= ALL_SPEC_INT)) then
			error("Minute and Second values must be between 0 and 59")
		end
	elseif (type == HOUR) then
		if ((val < 0 or val > 23 or vend > 23) and (val ~= ALL_SPEC_INT)) then
			error("Hour values must be between 0 and 23");
		end
	elseif (type == DAY_OF_MONTH) then
		if ((val < 1 or val > 31 or vend > 31) and (val ~= ALL_SPEC_INT) and (val ~= NO_SPEC_INT)) then
			error("Day of month values must be between 1 and 31");
		end
	elseif (type == MONTH) then
		if ((val < 1 or val > 12 or vend > 12) and (val ~= ALL_SPEC_INT)) then
			error("Month values must be between 1 and 12")
		end
	elseif (type == DAY_OF_WEEK) then
		if ((val == 0 or val > 7 or vend > 7) and (val ~= ALL_SPEC_INT) and (val ~= NO_SPEC_INT)) then
			error("Day-of-Week values must be between 1 and 7")
		end
	end

	if ((incr == 0 or incr == -1) and val ~= ALL_SPEC_INT) then
		if (val ~= -1) then
			bininsert(set,val);
		else
			bininsert(set,NO_SPEC);
		end
		return;
	end

	local startAt = val;
	local stopAt = vend;

	if (val == ALL_SPEC_INT and incr <= 0) then
		incr = 1;
		bininsert(set,ALL_SPEC); -- put in a marker, but also fill values
	end

	if (type == SECOND or type == MINUTE) then
		if (stopAt == -1) then
			stopAt = 59;
		end
		if (startAt == -1 or startAt == ALL_SPEC_INT) then
			startAt = 0;
		end
	elseif (type == HOUR) then
		if (stopAt == -1) then
			stopAt = 23;
		end
		if (startAt == -1 or startAt == ALL_SPEC_INT) then
			startAt = 0;
		end
	elseif (type == DAY_OF_MONTH) then
		if (stopAt == -1) then
			stopAt = 31;
		end
		if (startAt == -1 or startAt == ALL_SPEC_INT) then
			startAt = 1;
		end
	elseif (type == MONTH) then
		if (stopAt == -1) then
			stopAt = 12;
		end
		if (startAt == -1 or startAt == ALL_SPEC_INT) then
			startAt = 1;
		end
	elseif (type == DAY_OF_WEEK) then
		if (stopAt == -1) then
			stopAt = 7;
		end
		if (startAt == -1 or startAt == ALL_SPEC_INT) then
			startAt = 1;
		end
	elseif (type == YEAR) then
		if (stopAt == -1) then
			stopAt = MAX_YEAR;
		end
		if (startAt == -1 or startAt == ALL_SPEC_INT) then
			startAt = 1970;
		end
	end

	-- if the end of the range is before the start, then we need to overflow into 
	-- the next day, month etc. This is done by adding the maximum amount for that 
	-- type, and using modulus max to determine the value being added.
	local max = -1;
	if (stopAt < startAt) then
		if type==SECOND then
			max = 60
		elseif type==MINUTE then
			max = 60
		elseif type==HOUR then
			max=24
		elseif type==MONTH then
			max = 12
		elseif type==DAY_OF_WEEK then
			max = 7
		elseif type==YEAR then
			error("Start year must be less than stop year");
		else
			error("Unexpected type encountered");
		end
		stopAt = stopAt+max;
	end

	for i = startAt,stopAt,incr do
		if (max == -1) then
			-- ie: there's no max to overflow over
			bininsert(set,i);
		else
			-- take the modulus to get the real value
			local i2 = i % max;

			-- 1-indexed ranges should not include 0, and should include their max
			if (i2 == 0 and (type == MONTH or type == DAY_OF_WEEK or type == DAY_OF_MONTH) ) then
				i2 = max;
			end
			bininsert(set,i2);
		end
	end
end

local function checkNext(self, pos,  s,  val,  type)
	val=tonumber(val)
	local vend = -1;
	local i = pos;

	if (i > #s) then
		addToSet(self,val, vend, -1, type);
		return i;
	end

	local c = s:sub(pos,pos);

	if (c == 'L') then
		if (type == DAY_OF_WEEK) then
			if(val < 1 or val > 7) then
				error("Day-of-Week values must be between 1 and 7");
			end
			self.lastdayOfWeek = true;
		else
			error("'L' option is not valid here. (pos=" .. i .. ")");
		end
		local set = getSet(self,type);
		bininsert(set,val)
		i=i+1
		return i;
	end
	
	if (c == 'W') then
		if (type == DAY_OF_MONTH) then
			self.nearestWeekday = true;
		else
			error("'W' option is not valid here. (pos=" .. i .. ")")
		end
		if(val > 31) then
			error("The 'W' option does not make sense with values larger than 31 (max number of days in a month)")
		end 
		local set = getSet(self,type);
		bininsert(set,val)
		i=i+1
		return i;
	end

	if (c == '#') then
		if (type ~= DAY_OF_WEEK) then
			error("'#' option is not valid here. (pos=" .. i .. ")")
		end
		i=i+1
		self.nthdayOfWeek = tonumber(s:sub(i,i))
		if (self.nthdayOfWeek < 1 or self.nthdayOfWeek > 5) then
			error("A numeric value between 1 and 5 must follow the '#' option")
		end
		local set = getSet(self,type);
		bininsert(set,val)
		i=i+1
		return i;
	end

	if (c == '-') then
		i=i+1
		c = s:sub(i,i)
		local v = tonumber(c)
		vend = v;
		i=i+1
		if (i > #s) then
			addToSet(self,val, vend, 1, type);
			return i;
		end
		c = s:sub(i,i)
		if (c >= '0' and c <= '9') then
			local v1
			v1,i = getValue(v, s, i)
			vend = v1;
		end
		c = s:sub(i,i)
		if (i <= #s and (c == '/')) then
			i=i+1
			c = s:sub(i,i)
			local v2 = tonumber(c)
			i=i+1
			if (i > #s) then
				addToSet(self,val, vend, v2, type)
				return i;
			end
			c = s:sub(i,i)
			if (c >= '0' and c <= '9') then
				local v3
				v3,i = getValue(v2, s, i);
				addToSet(self,val, vend, v3, type);
				return i;
			else
				addToSet(self,val, vend, v2, type);
				return i;
			end
		else
			addToSet(self,val, vend, 1, type);
			return i;
		end
	end

	if (c == '/') then
		i=i+1
		c = s:sub(i,i)
		local v2 = tonumber(c);
		i=i+1
		if (i >#s) then
			addToSet(self,val, vend, v2, type);
			return i;
		end
		c = s:sub(i,i)
		if (c >= '0' and c <= '9') then
			local v3
			v3,i = getValue(v2, s, i);
			addToSet(self,val, vend, v3, type);
			return i;
		else
			error("Unexpected character '" .. c .. "' after '/'")
		end
	end

	addToSet(self,val, vend, 0, type);
	i=i+1
	return i;
end

	
local function storeExpressionVals(self,pos,s,type)
	local incr = 0;
	local i = string.find(s,'[^ \t]',pos)
	if not i then
		return #s+1
	end
	local c = s:sub(i,i)
	if (c >= 'A') and (c <= 'Z') and (s~="L") and (s~="LW") and (not s:match("^L%-[0-9]*[W]?")) then
		local sub = s:sub(i, i + 2);
		local sval = -1;
		local eval = -1;
		if (type == MONTH) then
			sval = monthMap[sub]
			if not sval then
				error("Invalid Month value: '" .. sub .. "'");
			end
			if (#s>=i + 3) then
				c = s:sub(i + 3,i+3);
				if (c == '-') then
					i=i+4;
					sub = s:sub(i, i + 2);
					eval = monthMap[sub]
					if not eval then
						error("Invalid Month value: '" .. sub .. "'");
					end
				end
			end
		elseif (type == DAY_OF_WEEK) then
			sval = dayMap[sub]
			if not sval then
				error("Invalid Day-of-Week value: '" .. sub .. "'");
			end
			if (#s>= i + 3) then
				c = s:sub(i + 3,i+3);
				if (c == '-') then
					i=i+4;
					sub = s:sub(i, i + 2);
					eval = dayMap[sub]
					if not eval then
						error("Invalid Day-of-Week value: '" .. sub .. "'");
					end
				elseif (c == '#') then
					i =i+ 4;
					self.nthdayOfWeek = s:sub(i,i);
					if (self.nthdayOfWeek < 1 or self.nthdayOfWeek > 5) then
						error("A numeric value between 1 and 5 must follow the '#' option")
					end
				elseif (c == 'L') then
					self.lastdayOfWeek = true;
					i=i+1
				end
			end
		else
			error("Illegal characters for this position: '" .. sub .. "'")
		end
		if (eval ~= -1) then
			incr = 1;
		end
		addToSet(self,sval, eval, incr, type);
		return (i + 3);
	end
	if (c == '?') then
		i=i+1
		if ((i + 1) <= #s and (s:sub(i,i) ~= ' ' or s:sub(i,i) ~= '\t')) then
			error("Illegal character after '?': ".. s.sub(i,i));
		end
		if (type ~= DAY_OF_WEEK and type ~= DAY_OF_MONTH) then
			error("'?' can only be specfied for Day-of-Month or Day-of-Week.")
		end
		if (type == DAY_OF_WEEK and not self.lastdayOfMonth) then
			local daysOfMonth=getSet(self,DAY_OF_MONTH)
			local val = daysOfMonth[#daysOfMonth]
			if (val == NO_SPEC_INT) then
				error("'?' can only be specfied for Day-of-Month -OR- Day-of-Week.")
			end
		end
	
		addToSet(self,NO_SPEC_INT, -1, 0, type);
		return i;
	end
	if (c == '*' or c == '/') then
		if (c == '*' and (i + 1) > #s) then
			addToSet(self,ALL_SPEC_INT, -1, incr, type);
			return i + 1;
		elseif (c == '/' and ((i + 1) > #s or s:sub(i + 1,i+1) == ' ' or s:sub(i + 1,i+1) == '\t')) then 
			error("'/' must be followed by an integer.");
		elseif (c == '*') then
			i=i+1
		end
		c = s:sub(i,i);
		if (c == '/') then -- is an increment specified?
			i=i+1
			if (i > #s) then
				error("Unexpected end of string.");
			end

			incr = tonumber(s:sub(i));

			i=i+1;
			if (incr > 10) then
				i=i+1;
			end
			if (incr > 59 and (type == SECOND or type == MINUTE)) then
				error("Increment > 60 : " .. incr);
			elseif (incr > 23 and (type == HOUR)) then 
				error("Increment > 24 : " .. incr)
			elseif (incr > 31 and (type == DAY_OF_MONTH)) then 
				error("Increment > 31 : " .. incr)
			elseif (incr > 7 and (type == DAY_OF_WEEK)) then 
				error("Increment > 7 : " .. incr)
			elseif (incr > 12 and (type == MONTH)) then
				error("Increment > 12 : " .. incr)
			end
		else
			incr = 1;
		end

		addToSet(self,ALL_SPEC_INT, -1, incr, type);
		return i;
	elseif (c == 'L') then
		i=i+1
		if (type == DAY_OF_MONTH) then
			self.lastdayOfMonth = true;
		end
		if (type == DAY_OF_WEEK) then
			addToSet(self,7, 7, 0, type);
		end
		if(type == DAY_OF_MONTH and #s >= i) then
			c = s:sub(i,i)
			if(c == '-') then
				self.lastdayOffset,i = getValue(0, s, i+1);
				if(self.lastdayOffset > 30) then
					error("Offset from last day must be <= 30");
				end
			end
			if #s >= i then
				c = s:sub(i,i)
				if(c == 'W') then
					self.nearestWeekday = true;
					i=i+1
				end
			end
		end
		return i;
	elseif (c >= '0' and c <= '9') then
		local val = c;
		i=i+1
		if (i > #s) then
			addToSet(self,val, -1, -1, type);
		else
			c = s:sub(i,i)
			if (c >= '0' and c <= '9') then
				val,i = getValue(val, s, i);
			end
			i = checkNext(self,i, s, val, type);
			return i;
		end
	else
		error("Unexpected character: " .. c);
	end

	return i;
end

local function buildExpression(self,expression)
	local exprOn = SECOND
	for expr in string.gmatch(expression,'[^ \t]+') do
		-- throw an exception if L is used with other days of the month
		if exprOn == DAY_OF_MONTH and expr:find('L') and #expr > 1 and expr:find(",") then
			error("Support for specifying 'L' and 'LW' with other days of the month is not implemented")
		end
		--throw an exception if L is used with other days of the week
		if exprOn == DAY_OF_WEEK and expr:find('L') and #expr > 1 and expr:find(",") then
			error("Support for specifying 'L' with other days of the week is not implemented")
		end
		
		if exprOn == DAY_OF_WEEK and expr:find('#') and expr:find('#',expr:find('#')+1) then
			error("Support for specifying multiple \"nth\" days is not imlemented.")
		end
		for v in string.gmatch(expr,'[^,]+') do
			storeExpressionVals(self,1, v, exprOn);
		end
		exprOn=exprOn+1;
	end
	if exprOn <= DAY_OF_WEEK then
		error("Unexpected end of expression.")
	end
	if exprOn <= YEAR then
		storeExpressionVals(self,1, "*", YEAR);
	end

	local dow = getSet(self,DAY_OF_WEEK);
	local dom = getSet(self,DAY_OF_MONTH);
	
	-- Copying the logic from the UnsupportedOperationException below
	local dayOfMSpec = not binsearch(dom,NO_SPEC);
	local dayOfWSpec = not binsearch(dow,NO_SPEC);
	
	if (dayOfMSpec and not dayOfWSpec) then
	
	elseif (dayOfWSpec and not dayOfMSpec) then 
	
	else
		error("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.")
	end
end

local function getExpressionSetSummary(set)
	if (binsearch(set,NO_SPEC)) then
		return "?";
	end
	if (binsearch(set,ALL_SPEC)) then
		return "*";
	end
	return table.concat(set,',')
end

local function getExpressionSummary(self)
	local buf={}

	table.insert(buf,"seconds: "..getExpressionSetSummary(getSet(self,SECOND)));
	table.insert(buf,"minutes: "..getExpressionSetSummary(getSet(self,MINUTE)));
	
	table.insert(buf,"hours: "..getExpressionSetSummary(getSet(self,HOUR)));
	
	table.insert(buf,"daysOfMonth: "..getExpressionSetSummary(getSet(self,DAY_OF_MONTH)));
	
	table.insert(buf,"months: "..getExpressionSetSummary(getSet(self,MONTH)));
	
	table.insert(buf,"daysOfWeek: "..getExpressionSetSummary(getSet(self,DAY_OF_WEEK)));
	
	table.insert(buf,"lastdayOfWeek: "..tostring(self.lastdayOfWeek));
	
	table.insert(buf,"nearestWeekday: "..tostring(self.nearestWeekday));
	
	table.insert(buf,"NthDayOfWeek: "..tostring(self.nthdayOfWeek));
	
	table.insert(buf,"lastdayOfMonth: "..tostring(self.lastdayOfMonth));
	
	table.insert(buf,"years: "..getExpressionSetSummary(getSet(self,YEAR)));
	

	return table.concat(buf,'\n')
end
local function getExpressionTest(self,from,cnt)
	from=from or os.time()
	cnt=cnt or 1
	print('start from now:',os.date("%Y%m%d %A %H:%M:%S",from))
	for i=1,cnt do
		from=self:getTimeAfter(from)
		if from then
			print('schedule',i,os.date("%Y%m%d %A %H:%M:%S",from))
		else
			break
		end
	end
end

local function isLeapYear(year)
	return ((year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0));
end
local function getLastDayOfMonth(m, year)
	if m==2 then
		return (isLeapYear(year)) and 29 or 28
	elseif m==1 or m==3 or m==5 or m==7 or m==8 or m==10 or m==12 then
		return 31
	elseif m>3 and m<12 then
		return 30
	end
	error("Illegal month number: ".. m)
end
local function normalizeDate(t)
	local v
	v=t.month
	if v>12 then
		t.month=(v-1)%12+1
		v=t.year+math.floor((v-1)/12)
		t.year=v
	else
	end
	
	v=t.sec
	if v>=60 then
		t.sec=v%60
		v=t.min+math.floor(v/60)
		t.min=v
	else
		v=t.min
	end
	if v>=60 then
		t.min=v%60
		v=t.hour+math.floor(v/60)
		t.hour=v
	else
		v=t.hour
	end
	if v>=24 then
		t.hour=v%24
		v=t.day+math.floor(v/24)
		t.day=v
	else
		v=t.day
	end
	
	local daysm=getLastDayOfMonth(t.month,t.year)
	if v> daysm then
		t.day=(v-1)%daysm+1
		v=t.month+math.floor((v-1)/daysm)
		t.month=v
	else
		v=t.month
	end
	if v>12 then
		t.month=(v-1)%12+1
		v=t.year+math.floor((v-1)/12)
		t.year=v
	else
	end
	local t2=os.date('*t',os.time(t))
	for k,v in pairs(t2) do
		t[k]=v
	end
end
function CornExp.getTimeAfter(self,afterTime)
	afterTime=afterTime+1
	local cl=os.date('*t',afterTime)
	local gotOne = false;
	local seconds=self._set[SECOND]
	local minutes=self._set[MINUTE]
	local hours=self._set[HOUR]
	local daysOfMonth=self._set[DAY_OF_MONTH]
	local daysOfWeek=self._set[DAY_OF_WEEK]
	local months=self._set[MONTH]
	local years=self._set[YEAR]
	while not gotOne do
		normalizeDate(cl)
		if cl.year>2999 then
			return
		end
repeat
		local st
		local t = 0;

		local sec = cl.sec
		local min = cl.min
		-- get second.................................................
		st = bintailset(seconds,sec)
		if (st <=#seconds) then
			sec = seconds[st]
		else
			sec = seconds[1];
			min=min+1;
			cl.min=min;
		end
		cl.sec=sec
		
		min = cl.min
		local hr = cl.hour
		t = -1;
		
		-- get minute.................................................
		st = bintailset(minutes,min);
		if (st <=#minutes) then
			t = min;
			min = minutes[st]
		else
			min = minutes[1]
			hr= hr+1
		end
		if (min ~= t) then
			cl.sec=0
			cl.min=min
			cl.hour=hr
			--continue;
			break
		end
		cl.min=min

		hr = cl.hour
		local day = cl.day
		t = -1;
		
		-- get hour...................................................
		st = bintailset(hours,hr)
		if (st <=#hours) then
			t = hr;
			hr = hours[st]
		else
			hr = hours[1]
			day=day+1
		end
		if (hr ~= t) then
			cl.sec=0
			cl.min=0
			cl.day=day
			cl.hour=hr
			break
		end
		cl.hour=hr

		day = cl.day
		local mon = cl.month
		t = -1;
		local tmon = mon;
		
		-- get day...................................................
		local dayOfMSpec = not binsearch(daysOfMonth,NO_SPEC);
		local dayOfWSpec = not binsearch(daysOfWeek,NO_SPEC);
		if (dayOfMSpec and not dayOfWSpec) then -- get day by day of month rule
			st = bintailset(daysOfMonth,day);
			if (self.lastdayOfMonth) then
				if(not self.nearestWeekday) then
					t = day;
					day = getLastDayOfMonth(mon, cl.year);
					day = day-self.lastdayOffset;
					if(t > day) then
						mon=mon+1;
						if(mon > 12) then 
							mon = 1;
							tmon = 3333; -- ensure test of mon != tmon further below fails
							cl.year=cl.year+1
						end
						day = 1;
					end
				else
					t = day;
					day = getLastDayOfMonth(mon, cl.year);
					day = day-self.lastdayOffset;

					local tcal=os.data('*t',os.time({year=cl.year,month=mon,day=day}))
					
					local ldom = getLastDayOfMonth(mon, cl.year);
					local dow = tcal.wday

					if(dow == 7 and day == 1) then
						day = day+2;
					elseif(dow == 7) then
						day = day-1;
					elseif(dow == 1 and day == ldom) then
						day = day-2;
					elseif(dow == 1) then
						day = day+1;
					end
				
					tcal.sec=sec
					tcal.min=min
					tcal.hour=hr
					tcal.day=day
					tcal.mon=mon
					
					if(os.time(tcal) < afterTime) then
						day = 1;
						mon=mon+1;
					end
				end
			elseif(nearestWeekday) then
				t = day;
				day = daysOfMonth[1]
				
				local tcal=os.data('*t',os.time({year=cl.year,month=mon,day=day}))

				local ldom = getLastDayOfMonth(mon, cl.year);
				local dow = tcal.wday
				if(dow == 7 and day == 1) then
					day = day+2;
				elseif(dow == 7) then
					day = day-1;
				elseif(dow == 1 and day == ldom) then
					day = day-2;
				elseif(dow == 1) then
					day = day+1;
				end
			
				tcal.sec=sec
				tcal.min=min
				tcal.hour=hr
				tcal.day=day
				tcal.mon=mon
				if(os.time(tcal) < afterTime) then
					day = daysOfMonth[1]
					mon=mon+1;
				end
			elseif (st <=#daysOfMonth) then
				t = day;
				day = daysOfMonth[st]
				-- make sure we don't over-run a short month, such as february
				local lastDay = getLastDayOfMonth(mon, cl.year);
				if (day > lastDay) then
					day = daysOfMonth[1]
					mon=mon+1
				end
			else
				day = daysOfMonth[1]
				mon=mon+1
			end
			if (day ~= t or mon ~= tmon) then
				cl.sec=0
				cl.min=0
				cl.hour=0
				cl.day=day
				cl.month=mon
				--continue;
				break
			end
		elseif (dayOfWSpec and not dayOfMSpec) then -- get day by day of week rule
			if (self.lastdayOfWeek) then -- are we looking for the last XXX day of
				-- the month?
				local dow = daysOfWeek[1] -- desired
				-- d-o-w
				local cDow = cl.wday; -- current d-o-w
				local daysToAdd = 0;
				if (cDow < dow) then
					daysToAdd = dow - cDow;
				end
				if (cDow > dow) then
					daysToAdd = dow + (7 - cDow);
				end

				local lDay = getLastDayOfMonth(mon, cl.year);

				if (day + daysToAdd > lDay) then -- did we already miss the
					-- last one?
					cl.sec=0
					cl.min=0
					cl.hour=0
					cl.day=1
					cl.month=mon+1
					--continue;
					break
				end

				-- find date of last occurrence of this day in this month...
				while ((day + daysToAdd + 7) <= lDay) do
					daysToAdd = daysToAdd+7;
				end

				day = day+daysToAdd;

				if (daysToAdd > 0) then
					cl.sec=0
					cl.min=0
					cl.hour=0
					cl.day=day
					cl.month=mon
					--continue;
					break
				end
			elseif (self.nthdayOfWeek ~= 0) then
				-- are we looking for the Nth XXX day in the month?
				local dow = daysOfWeek[1] -- desired
				-- d-o-w
				local cDow = cl.wday; -- current d-o-w
				local daysToAdd = 0;
				if (cDow < dow) then
					daysToAdd = dow - cDow;
				elseif (cDow > dow) then
					daysToAdd = dow + (7 - cDow);
				end

				local dayShifted = false;
				if (daysToAdd > 0) then
					dayShifted = true;
				end

				day = day+daysToAdd;
				local weekOfMonth = math.floor(day / 7);
				if (day % 7 > 0) then
					weekOfMonth=weekOfMonth+1;
				end

				daysToAdd = (self.nthdayOfWeek - weekOfMonth) * 7;
				day = day+daysToAdd;
				if (daysToAdd < 0 or day > getLastDayOfMonth(mon, cl.year)) then
					cl.sec=0
					cl.min=0
					cl.hour=0
					cl.day=1
					cl.month=mon+1
					--continue;
					break
				elseif (daysToAdd > 0 or dayShifted) then
					cl.sec=0
					cl.min=0
					cl.hour=0
					cl.day=day
					cl.month=mon
					--continue;
					break
				end
			else
				local cDow = cl.wday; -- current d-o-w
				local dow = daysOfWeek[1]; -- desired
				-- d-o-w
				st = bintailset(daysOfWeek,cDow)
				if (st <= #daysOfWeek) then
					dow = daysOfWeek[st]
				end

				local daysToAdd = 0;
				if (cDow < dow) then
					daysToAdd = dow - cDow;
				end
				if (cDow > dow) then
					daysToAdd = dow + (7 - cDow);
				end

				local lDay = getLastDayOfMonth(mon, cl.year);

				if (day + daysToAdd > lDay) then -- will we pass the end of
					-- the month?
					cl.sec=0
					cl.min=0
					cl.hour=0
					cl.day=1
					cl.month=mon+1
					--continue;
					break
				elseif (daysToAdd > 0) then -- are we swithing days?
					cl.sec=0
					cl.min=0
					cl.hour=0
					cl.day=day + daysToAdd
					cl.month=mon
					--continue;
					break
				end
			end
		else -- dayOfWSpec && !dayOfMSpec
			error("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
			-- TODO:
		end
		cl.day=day

		mon = cl.month
		local year = cl.year
		t = -1;

		if (year > MAX_YEAR) then
			return 
		end
		
		-- get month...................................................
		st = bintailset(months,mon)
		if (st <=#months) then
			t = mon;
			mon = months[st]
		else
			mon = months[1]
			year=year+1;
		end
		if (mon ~= t) then
			cl.sec=0
			cl.min=0
			cl.hour=0
			cl.day=1
			cl.month=mon
			cl.year=year
			--continue;
			break
		end
		cl.month= mon
		year = cl.year
		t = -1;
		
		-- get year...................................................
		st = bintailset(years,year)
		if (st <=#years) then
			t = year;
			year = years[st]
		else
			return
		end

		if (year ~= t) then
			cl.sec=0
			cl.min=0
			cl.hour=0
			cl.day=1
			cl.month=1
			cl.year=year
			--continue;
			break
		end
		cl.year=year
		gotOne = true;
		return os.time(cl)
until true
	end
	
end

--test
if false then
local teststr={
{'0 15 10 * * ? *','每天上午10:15触发'},
{'0 0 11 4 * ?', '每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点'},
{'0 15 10 * * ? 2015','2015年的每天上午10:15触发'},
{'0 15 10 ? * MON-FRI','周一至周五的上午10:15触发'},
{'0 15 10 ? * 6L','每月的最后一个星期五上午10:15触发'},
{'0 15 10 ? * 6L 2002-2022','2002年至2022年的每月的最后一个星期五上午10:15触发'},
{"0 15 10 ? * 6#3",'每月的第三个星期五上午10:15触发'},
{"0 15 10 L * ?",'每月最后一日的上午10:15触发'},
}
for k,v in pairs(teststr) do
	print(v[2])
	local tst=new(v[1])
	buildExpression(tst,tst.cronExpression)
	print(getExpressionSummary(tst))
	getExpressionTest(tst,nil,10)
end
end